<!--
  Test Harness for MobWrite Server

  Copyright (C) March 2008 Google Inc.
  http://code.google.com/p/google-mobwrite/

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
-->

<html>
  <head>
    <TITLE>Test Harness for MobWrite Server</TITLE>

    <SCRIPT SRC="../diff_match_patch_uncompressed.js"></SCRIPT>
    <script type="text/javascript"><!--
      // List of tests to run.
      var tests;
      // Index of current test.
      var testIndex;
      // Index of current node in current test.
      var partIndex;
      // The text previously sent to the server.
      var lastQuestion;
      // The text previously received from the server.
      var lastAnswer;
      // A unique identifier to prevent collisions between unit tests.
      var randomId;

      // Counters for unit test results.
      var test_good, test_bad;
      // Boolean for if we have ever received a response.
      var gotResponse;

      // Current Ajax object in flight.
      var ajaxObject = null;
      // Length of time to wait for an Ajax response.
      var ajaxTimeoutPeriod = 10 * 1000;
      // Watchdog task to abort stalled Ajax.
      var ajaxTimeoutPid = 0;


      // Add a line of output to the screen.
      function writeOutput(html) {
        var node = document.createElement('DIV');
        node.innerHTML = html;
        document.getElementById('outputDiv').appendChild(node);
      }


      // Raw method for requesting XMLHttpRequest data.
      function requestAjax(url, post) {
        var req = null;
        // branch for native XMLHttpRequest object
        if (window.XMLHttpRequest) {
          try {
            req = new XMLHttpRequest();
          } catch(e) {
            req = null;
          }
          // branch for IE/Windows ActiveX version
        } else if (window.ActiveXObject) {
          try {
            req = new ActiveXObject('Msxml2.XMLHTTP');
          } catch(e) {
            try {
              req = new ActiveXObject('Microsoft.XMLHTTP');
            } catch(e) {
            	req = null;
            }
          }
        }
        if (req) {
          req.onreadystatechange = receiveAjax;
          try {
            req.open('POST', url, true);
            req.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
            req.send(post);
            ajaxTimeoutPid = window.setTimeout('ajaxTimeoutAbort()', ajaxTimeoutPeriod);
            // Execution will resume in receiveAjax after the callback.
          } catch(e) {
            alert('Unable to open XMLHttpRequest:\n' + url + '\n' + e);
          }
        } else {
          alert('Unable to create XMLHttpRequest:\n' + url);
        }
        ajaxObject = req;
      }


      // Callback method for receiving XMLHttpRequest data.
      function receiveAjax() {
        if (!ajaxObject) {
          return;
        }
        // Only if state is "loaded"
        if (ajaxObject.readyState == 4) {
          // Cancel the watchdog.
          window.clearTimeout(ajaxTimeoutPid);
          if (!tests) {
            // This must be the test data.
            var xml = ajaxObject.responseXML;
            ajaxObject = null;
            startTests(xml);
          } else {
            // This must be a test answer.
            var text = ajaxObject.responseText;
            ajaxObject = null;
            collectAnswer(text);
          }
        }
      }


      // XMLHttpRequest took too long.  Terminate.
      function ajaxTimeoutAbort() {
        alert('Timeout of XMLHttpRequest.');
        ajaxObject.abort();
        ajaxObject = null;
      }


      // Start the initialization.
      function initTests() {
        // Clear and show the output display.
        document.getElementById('outputDiv').innerHTML = '';
        document.getElementById('outputFieldSet').style.display = 'block';
        // Request the test data sets.
        tests = null;
        var dataUrl = document.getElementById('dataUrl').value;
        writeOutput('Loading data from <A HREF="' + dataUrl + '">' + dataUrl + '</A>...');
        requestAjax(dataUrl, '');
        // Execution will resume in startTests after the callback.
      }


      // Start the tests.
      function startTests(xml) {
        tests = xml.getElementsByTagName('TEST');
        writeOutput('...found ' + tests.length + ' test groups.<BR><BR>');
        test_good = 0;
        test_bad = 0;
        gotResponse = false;
        testIndex = 0;
        nextGroup();
        nextTest();
      }


      function nextGroup() {
        // Reset the global variables for the next group.
        partIndex = 0;
        lastQuestion = null;
        lastAnswer = null;
        randomId = String(Math.random()).substring(2);
      }


      // Post the next test question.
      function nextTest() {
        if (testIndex >= tests.length) {
          // No more tests, finish.
          endTests();
          return;
        }

        var testObj = tests[testIndex];
        if (partIndex >= testObj.childNodes.length) {
          // No more parts to this test.  Reset and move to next one.
          testIndex++;
          nextGroup();
          nextTest();
          return;
        }

        var name = testObj.getAttribute('NAME');
        var node = testObj.childNodes[partIndex];
        if (node.nodeName == '#text' || node.nodeName == '#comment') {
          // Skip.  Text will just be whitespace.
        } else if (node.nodeName == 'QUESTION') {
          // Extract the next question.
          lastQuestion = getTextFromChild(testObj, partIndex);
          // Send the question to the server.
          var serverUrl = document.getElementById('serverUrl').value;
          var post = 'q=' + encodeURIComponent(lastQuestion);
          requestAjax(serverUrl, post);
          partIndex++;
          // Execution will resume in collectAnswer after the callback.
          return;
        } else if (node.nodeName == 'ANSWER') {
          // Extract the expected answer.
          var expectedAnswer = getTextFromChild(testObj, partIndex);

          // Verify that the answer was correct.
          if (expectedAnswer === lastAnswer) {
            writeOutput(name + ': <FONT COLOR="#009900">Ok</FONT>');
            test_good++;
          } else {
            writeOutput(name + ': <FONT COLOR="#990000"><BIG>Fail!</BIG></FONT>');
            // Compute a diff between the expected and the actual answer.
            var diffString;
            try {
              // The diff requires an external dependancy.  Ensure that if this
              // fails the page continues to function.
              var dmp = new diff_match_patch();
              var diff = dmp.diff_main(expectedAnswer, lastAnswer);
              dmp.diff_cleanupSemantic(diff);
              diffString = dmp.diff_prettyHtml(diff);
            } catch(e) {
              diffString = e;
            }
            var msg = '<TABLE BORDER=1 STYLE="margin-left: 20px">' +
                '<TR><TD>Question:</TD><TD>Expected:</TD><TD>Actual:</TD><TD>Diff:</TD></TR>\n';
            msg += '<TR><TD VALIGN="top"><PRE>' + htmlEscape(lastQuestion) + '</PRE></TD>' +
                '<TD VALIGN="top"><PRE>' + htmlEscape(expectedAnswer) + '</PRE></TD>' +
                '<TD VALIGN="top"><PRE>' + htmlEscape(lastAnswer) + '</PRE></TD>' +
                '<TD VALIGN="top"><PRE>' + diffString + '</PRE></TD></TR></TABLE>';
            writeOutput(msg);
            test_bad++;
          }

          // Make a note of whether we ever got any response.
          if (lastAnswer) {
            gotResponse = true;
          }
        } else {
          // Some unsupported node.
          writeOutput(node.nodeName + ': <FONT COLOR="#990000"><BIG>Unknown tag type.</BIG></FONT>');
        }

        // Call the next question part.
        partIndex++;
        window.setTimeout('nextTest();', 1);
      }


      // Evaluate the answer of the previous question.
      function collectAnswer(actualAnswer) {
        lastAnswer = formatText(actualAnswer);
        nextTest();
      }


      // End the test run and print results.
      function endTests() {
        writeOutput('<BR>Tests passed: ' + test_good + '<BR>Tests failed: ' + test_bad);
        if (!gotResponse) {
          // The printed test results are meaningless.  Clear the output.
          document.getElementById('outputDiv').innerHTML = '';
          writeOutput('<SPAN STYLE="background-color: yellow">Error: No response from the server was ever received to any question.  Check that the daemon is running.</SPAN>');
        }
      }


      // Return the text contents of the nth child.
      // e.g. xmlNode is <PARENT><SON>John</SON><DAUGHTER>Mary</DAUGHTER></PARENT>
      // getTextFromChild(xmlNode, 0) -> 'John'
      function getTextFromChild(xmlNode, childIndex) {
        var childXmlNode = xmlNode.childNodes[childIndex];
        var text = '';
        for (var y = 0, grandchildXmlNode; grandchildXmlNode = childXmlNode.childNodes[y]; y++) {
          text += grandchildXmlNode.nodeValue;
        }
        return formatText(text);
      }


      // HTML escape a string.
      function htmlEscape(text) {
        return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
      }


      // XML often looks like:
      //   <XML>
      // foobar
      //   <XML>
      // Convert "\nfoobar\n  " to "foobar\n"
      // Also replace [RANDOM] with the random ID for this session.
      function formatText(text) {
        text = text.replace(/[ \t]*$/, '');
        text = text.replace(/^\s*(\n\r|\r\n|\r|\n)/, '');
        text = text.replace(/\[RANDOM\]/g, randomId);
        return text;
      }


      // Unit test for a unit test (yes, I'm that paranoid).
      randomId = 'XYZ';
      if (formatText('\nfoo\n   ') != 'foo\n' || formatText('\n\n   ') != '') {
        alert('formatText does not strip whitespace properly.');
      }
      if (formatText('abc[RANDOM]def[RANDOM]ghi') != 'abcXYZdefXYZghi') {
        alert('formatText does not insert [RANDOM].');
      }
      randomId = undefined;

    //--></script>
  </head>
  <body>
    <H1>Test Harness for MobWrite Server</H1>

    <script type="text/javascript"><!--
      if (window.location.protocol == 'file:') {
        document.write('<P STYLE="background-color: yellow;">WARNING: These tests will not run from a file://        URL.  You should load this file using http:// through your webserver.</P>');
      }
    //--></script>

    <FORM onsubmit="initTests(); return false;" ACTION="javascript:alert('JavaScript Error');">
      <TABLE>
        <TR><TD>Data:</TD><TD><INPUT ID="dataUrl" VALUE="server.xml" SIZE="40"></TD></TR>
        <TR><TD>Server:</TD><TD><INPUT ID="serverUrl" VALUE="/scripts/q.py" SIZE="40"></TD></TR>
        <TR><TD></TD><TD><INPUT TYPE="submit" VALUE="Run"></TD></TR>
      </TABLE>
    </FORM>

    <FIELDSET ID="outputFieldSet" STYLE="display: none"><LEGEND>Output:</LEGEND>
      <DIV ID="outputDiv"></DIV>
    </FIELDSET>

  </body>
</html>
